#!/usr/bin/env node
const camo = require('camo')
const discord = require('discord.js')
const _ = require('lodash')
const config = require('./config')
const Log = require('./util/log')
const chalk = require('chalk')
const commandExists = require('command-exists')

const EmoteStore = require('./stores/emote')
const Character = require('./character')
const Battle = require('./battle/battle')
const Move = require('./battle/move')
const Party = require('./character/party')
const { DiscordAI } = require('./battle/ai')

class Game {
  // Starts the game. Should only run once per process!
  async boot() {
    // Create an instance of the logging tool
    this.log = new Log()

    // Check graphicsmagick is installed
    if (!await commandExists('gm')) {
      this.log.warning('GraphicsMagick not found - please install it')
    }

    // Load the config file
    await config.load()

    // Connect to the database (nedb or mongodb)
    await camo.connect(config.get('database_uri'))

    // Create the bot pool
    this.clientPool = await Promise.all(config.get('discord_bot_tokens').map(async token => {
      const client = new discord.Client()

      try {
        await client.login(token)
      } catch (err) {
        this.log.critical('Bot login failure (is the token correct?)')
        return null
      }

      // Check the bot is actually in all the guilds (servers) we will be using
      let inAllGuilds = true
      const guildIDs = [
        config.get('discord_server_id'),
        ...config.get('discord_emote_store_server_ids'),
      ]

      for (const guildID of guildIDs) {
        if (!client.guilds.get(guildID)) {
          inAllGuilds = false
        }
      }

      if (inAllGuilds) {
        this.log.ok(`Bot ${chalk.blue(client.user.tag)} logged in successfully`)

        return client
      } else {
        const url = `https://discordapp.com/oauth2/authorize?&client_id=${client.user.id}&scope=bot&permissions=${0x00000008}&response_type=code`
        this.log.warning(`Bot ${chalk.blue(client.user.tag)} not connected to configured Discord server(s) - add it using the following URL:\n${chalk.underline(url)}`)
        return null
      }
    })).then(pool => pool.filter(client => client !== null))

    if (this.clientPool.length === 0) {
      throw 'No bots connected, cannot start'
    }

    // Setup stores
    this.emoteStore = new EmoteStore(this, config.get('discord_emote_store_server_ids'))

    // Cleanup temporary channels (incase of crash last time)
    await this.emoteStore.cleanUp()
    await Promise.all(this.guild.channels
      .filter(chnl => chnl.name === 'battle')
      .map(chnl => chnl.delete()))

    // Clean dead roles
    await Party.cleanDeadRoles(this.guild, this.log)

    // TEMP: Create a couple moves to give to new characters.
    const pokeMove = await Move.upsert('👉', 'Poke', 'Deals a tiny amount of damage to a single enemy.', {
      target: 'enemy',
      actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
    })

    const kissMove = await Move.upsert('💋', 'Kiss', 'Heals a party member by a tiny amount.', {
      target: 'party',
      actions: [{type: 'heal', data: {amount: 3}, to: 'target'}],
      basePrepareTicks: 1,
    })

    const multipokeMove = await Move.upsert('👏', 'Multipoke', 'Deals a tiny amount of damage to up to three enemies at once.', {
      target: Move.TargetDesc.of('enemy', 3),
      actions: [{type: 'damage', data: {amount: 2}, to: 'target'}],
      baseCooldownTicks: 2,
    })

    // Add all players to the database (if they don't exist already)
    // This could take a while on large servers
    //
    // TODO: add new users (memberadd event) as characters when they join if the
    // server is already running
    await Promise.all(this.guild.members.filter(m => !m.user.bot).map(async member => {
      let char = await Character.findOne({discordID: member.id})

      if (!char) {
        char = await Character.create({
          discordID: member.id,
          battleAI: 'DiscordAI',
          moveIDs: [pokeMove._id, multipokeMove._id, kissMove._id],
        }).save()

        this.log.info(`Created player data for new user ${chalk.blue(member.user.tag)}`)

        await char.healthUpdate(this.guild)
      }
    }))

    // TEMP
    this.clientPool[0].on('message', async msg => {
      if (msg.guild.id !== config.get('discord_server_id')) return
      if (msg.author.bot) return

      const self = await Character.findOne({discordID: msg.author.id})

      // Usage: .fight @opponent#0001 @opponent#0002 [...]
      // Initiates a fight between your party and the parties of the specified
      // opponents.
      if (msg.content.startsWith('.fight ')) {
        const battle = new Battle(this)

        // FIXME: this crashes if the battle starts off completed (eg. one team
        // is comprised of only dead players).

        const characters = await Promise.all(msg.mentions.users.map(user => Character.findOne({discordID: user.id})))

        const parties = _.uniq([
          await self.getParty(), ...await Promise.all(characters.map(character => character && character.getParty()))
        ])

        for (const party of parties) {
          await battle.addTeam(party)
        }

        while (await battle.tick()) {
          // Use this to mess with the battle as it runs.
        }
      }

      // Usage: .revive
      if (msg.content === '.revive') {
        self.health = self.maxHealth
        await self.healthUpdate(this.guild)
      }

      // Usage: .suicide
      if (msg.content === '.suicide') {
        self.health = 0
        await self.healthUpdate(this.guild)
      }

      // Usage: .emote @user#0001 @user#0002 [...]
      if (msg.content.startsWith('.emote ')) {
        for (const [ userID, user ] of msg.mentions.users) {
          const char = await Character.findOne({discordID: userID})
          const emote = await char.getEmote(this)

          await msg.channel.send(`${emote}`)
        }
      }
    })
  }

  get guild() {
    return _.sample(this.clientPool).guilds.get(config.get('discord_server_id'))
  }
}

// Let's go!!
const game = new Game()

game.boot()
  .then(() => game.log.ok('Game started'))
  .catch(err => {
    // :(

    game.log.critical('Unhandled error during boot:\n' + (err.message || err))

    process.exit(1)
  })
